package com.acme.javaschool.intense.sems.sem2concurrency.pool;

import com.acme.javaschool.intense.sems.sem2concurrency.pool.data.ObjectSource;
import com.acme.javaschool.intense.sems.sem2concurrency.pool.data.PoolableObject;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphorePool<T> extends BasePool<T> {
    private Semaphore semaphore;
    private ConcurrentHashMap<PoolableObject<T>, Boolean> items;

    int availableItemsCount;
    int bound;

    public SemaphorePool(ObjectSource<T> source, int minBound, int maxBound) {
        super(source, minBound, maxBound);
        semaphore = new Semaphore(maxBound);
        items = new ConcurrentHashMap<>();
        fillAvailableItems(minBound);
        availableItemsCount = minBound;
        bound = minBound;
    }

    @Override
    protected void shrink() {
        try {
            lock.lock();
            if (getTakenItemsCount() > minBound) {
                bound = getTakenItemsCount();
                availableItemsCount = 0;
                for(Map.Entry<PoolableObject<T>, Boolean> entry : items.entrySet()){
                    items.remove(entry.getKey(), Boolean.TRUE);
                }
                items = new ConcurrentHashMap<>(items);
            } else {
                int count = getAvailableItemsCount() - (minBound - getTakenItemsCount());
                bound = minBound;
                for(Map.Entry<PoolableObject<T>, Boolean> entry : items.entrySet()){
                    if (count == 0) {
                        break;
                    }
                    items.remove(entry.getKey(), Boolean.TRUE);
                    count--;
                    availableItemsCount--;
                }
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    protected void expand() {
        int sizeDelta = maxBound - getBound();
        bound = maxBound;
        availableItemsCount += sizeDelta;
        // should preserve space for the already issued items.
        fillAvailableItems(sizeDelta);
    }


    @Override
    public PoolableObject<T> tryGet(long timeoutMillis) throws InterruptedException {
        try {
            lock.lock();
            PoolableObject<T> item = null;

            resizeIfNeeded();

            if (timeoutMillis >= 0) {
                if (semaphore.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)) {
                    item = getAvailableItem();
                }
            } else {
                semaphore.acquire();
                item = getAvailableItem();
            }

            if (item != null) {
                availableItemsCount--;
                if (getAvailableItemsCount() == shrinkSizeThreshold + 1) {
                    lastThresholdSizeTimeMillis = System.currentTimeMillis();
                }
                item.open();
            }
            return item;

        } finally {
            lock.unlock();
        }
    }

    private PoolableObject<T> getAvailableItem() {
        for(Map.Entry<PoolableObject<T>, Boolean> entry : items.entrySet()){
            if (entry.getValue().equals(Boolean.TRUE)) {
                entry.setValue(Boolean.FALSE);
                return entry.getKey();
            }
        }
        return null;
    }

    @Override
    public void release(PoolableObject<T> item) throws InterruptedException {
        if (item == null)
            return;

        item.close();
        availableItemsCount++;
        if (!items.replace(item, Boolean.FALSE, Boolean.TRUE)) {
            throw new IllegalStateException("release: Item not present or not taken.");
        }
        semaphore.release();
    }

    @Override
    protected int getTakenItemsCount() {
        return bound - availableItemsCount;
    }

    @Override
    public int getAvailableItemsCount() {
        return availableItemsCount;
    }

    @Override
    public int getBound() {
        return bound;
    }

    @Override
    public int getMaxBound() {
        return maxBound;
    }

    protected void fillAvailableItems(int sizeDelta) {
        for (int i = 0; i < sizeDelta; i++) {
            items.put(source.newInstance(), Boolean.TRUE);
        }
    }

}
